/* Widget.js
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

$package("dowry.widget");

/**
 * Object representation of a UI widget.
 *
 * <p>
 * This base widget may be used as a default for most
 * non-input fields... particularly "block elements", such
 * as <code>&lt;span&gt;</code>, <code>&lt;div&gt;</code>,
 * <code>&lt;p&gt;</code>, etc.
 * </p>
 *
 */
$class("Widget").$as(
{
    /**
     * Constant for the CSS class name to apply when a field
     * is invalid.
     *
     */
    CSS_INVALID : "invalid",

    /**
     * Constant for the CSS class name to apply to span
     * containing required indicator.
     *
     */
    CSS_REQUIRED : "required",

    /**
     * Constant for the HTML to present when a field is
     * required.
     *
     */
    REQUIRED_SYMBOL : "*",

    /**
     * Property representing the string ID of the element
     * bound to this datatype. This property is set during
     * <code>initialize()</code>, and should considered
     * "read-only".
     *
     */
    _ele: null,

    /**
     * Property representing the error message associated
     * with this widget.
     *
     */
    _errorMessage : null,

    /**
     * When a widget is constructed, it is associated with
     * an existing HTML element.  As part of object
     * instantation, the Widget should perform any additonal
     * basic initialization required for this widget.  This
     * includes any modifications to the DOM for the current
     * page to fully render the widget.
     *
     * @param ele    the element (or ID of the element)
     *               to bind to this widget
     *
     */
    Widget : function(ele)
    {
        ele = $(ele);

        if (ele && ele.id)
        {
            this._ele = ele.id;
            ele.dowryWidget = this;
        }
    },

    /**
     * Gets the datatype that is bound to this widget's
     * element.
     *
     * @return  the datatype associated with this widget
     *
     */
    getDatatype : function()
    {
        var dt = null;

        var ele = this.getElement();
        if (ele)
        {
            dt = ele.dowryDatatype;
        }

        return dt;
    },

    /**
     * Gets the element associated with this widget.
     *
     * @return  the element associated with this widget
     *
     */
    getElement : function()
    {
        return $(this._ele);
    },

    /**
     * Gets the error message on the widget.
     *
     * @return    the message
     *
     */
    getErrorMessage : function()
    {
        return this._errorMessage;
    },

    /**
     * Gets the value of the bound element, represented as a
     * type appropriate for the element's datatype.
     *
     * @return  the value
     *
     */
    getValue : function()
    {
        var val = null;

        var ele = this.getElement();
        if (ele)
        {
            val = this._getValueImpl(ele);

            // convert the raw value
            var dt = this.getDatatype();
            if (dt)
            {
                val = dt.toDatatype(val);
            }
        }

        return val;
    },

   /**
     * Sets the error message on the widget.  If a non-null
     * error message is provided, this will make the UI
     * field display as <code>invalid</code> with the
     * message appearing in the tooltip.  A null message
     * will remove any error currently being displayed.
     *
     * @param  msg   the message
     *
     */
    setErrorMessage : function(msg)
    {
        var ele = this.getElement();
        this._setErrorMessageImpl(ele, msg);
    },

    /**
     * Sets the value of the bound element, using a type
     * appropriate for the element's datatype.
     *
     * @param val  the value to set
     *
     */
    setValue : function(val)
    {
        var ele = this.getElement();
        if (ele)
        {
            // convert the provided value
            var dt = this.getDatatype();
            if (dt)
            {
                val = dt.toFormattedString(val);
            }

            this._setValueImpl(ele, val);
        }
    },

    /**
     * Starts a widget.  This is usually where event
     * handlers are associated, etc... anyting that needs
     * to happen after initialization begins.
     *
     * <p>
     * In this default implementation, start() does
     * nothing.
     * </p>
     *
     */
    start : function()
    {
    },

    /**
     * Validates the value of the widget, according to the
     * associated datatype, and updates the widget's error
     * message as appropriate.
     *
     */
    validate : function()
    {
        var ele = this.getElement();
        var dt = this.getDatatype();

        if (ele && !ele.disabled && dt)
        {
            var val = this.getValue();

            try
            {
                var msg = dt.validate(val);
                this.setErrorMessage(msg);
            }
            catch(e)
            {
                // handle remote validation
                if (e == dt.REMOTE_VALIDATE)
                {
                    var cfg = dt.toConfiguration();
                    cfg.id = ele.id;
                    cfg.value = val;
                    Dowry.remote.validate(cfg, this.setErrorMessage);
                }
                else
                {
                    throw e;
                }
            }
        }
    },

    /**
     * Adds the required symbol to the UI, if the widget
     * is associated with a datatype that is required.
     *
     * @param eleOrSpan the element itself (or the enclosing
     *                  span, if true is passed in the 2nd
     *                  parameter)
     *
     * @param isSpan    boolean indicating that the span
     *                  is already provided... so don't
     *                  create one (optional... if not
     *                  provided, it will be assumed as
     *                  false)
     *
     */
    _addAsteriskIfRequired : function(eleOrSpan, isSpan)
    {
        var dt = this.getDatatype();
        if (eleOrSpan && dt && dt.required)
        {
            if (!isSpan)
            {
                eleOrSpan = this._createEnclosingSpan(eleOrSpan);
            }

            var required = document.createElement("span");
            Dowry.css.addClassName(required, this.CSS_REQUIRED);
            required.appendChild(document.createTextNode(this.REQUIRED_SYMBOL));
            eleOrSpan.appendChild(required);
        }
    },

    /**
     * Creates a span that encloses the specified element.
     *
     * @param ele   the element to be enclosed
     *
     * @return      the span
     *
     */
    _createEnclosingSpan : function(ele)
    {
        var span = null;

        if (ele)
        {
            var parentNode = ele.parentNode;
            span = document.createElement("span");
            parentNode.replaceChild(span, ele);
            span.appendChild(ele);
        }

        return span;
    },

    /**
     * Base handling of getting the raw value from the
     * specified element.  We'll put this in its own
     * method, so we can reuse it elsewhere.
     *
     * @param ele   the element to get value from
     *
     * @return      the element's value (the inner content)
     *
     */
    _getValueImpl : function(ele)
    {
        var val = null;

        if (ele && ele.childNodes)
        {
            var textNodes = new Array();
            for (var i=0; i < ele.childNodes.length; i++)
            {
                // text node
                if (ele.childNodes[i].nodeType == 3)
                {
                    textNodes[textNodes.length] =
                        ele.childNodes[i].nodeValue;
                }
            }

            // append the text nodes together
            if (textNodes.length > 0)
            {
                val = textNodes.join("");
            }
        }

        return val;
    },

    /**
     * Copied from DWRUtil.  Gets around missing
     * functionality in IE.
     *
     * @author               Joe Walker
     *                       <http://getahead.ltd.uk/dwr/>
     *
     * @param doc            the document
     *
     * @param importedNode   the node to import
     *
     * @param deep           boolean indicating whether
     *                       or not to perform a deep copy
     *
     * @return               the new, imported node
     *
     */
    _importNode : function(doc, importedNode, deep)
    {
        var newNode;

        // if it is a newly created node for this
        // document (eg. doc.createElement...) then we
        // don't really need to import anything
        if (importedNode.ownerDocument == null ||
            doc == importedNode.ownerDocument)
        {
            newNode = importedNode;
        }

        // if this node belongs to another document,
        // or if this node is already in the dom tree
        // somewhere, then it needs to be imported...
        else
        {
            // element
            if (importedNode.nodeType == 1)
            {
                newNode = doc.createElement(importedNode.nodeName);

                for (var i = 0; i < importedNode.attributes.length; i++)
                {
                    var attr = importedNode.attributes[i];
                    if (attr.nodeValue != null && attr.nodeValue != '')
                    {
                        newNode.setAttribute(attr.name, attr.nodeValue);
                    }
                }

                if (typeof importedNode.style != "undefined")
                {
                    newNode.style.cssText = importedNode.style.cssText;
                }
            }

            // text node
            else if (importedNode.nodeType == 3)
            {
                newNode = doc.createTextNode(importedNode.nodeValue);
            }

            if (deep && importedNode.hasChildNodes())
            {
                for (var i = 0; i < importedNode.childNodes.length; i++)
                {
                    newNode.appendChild(
                        this._importNode(doc, importedNode.childNodes[i], true));
                }
            }
        }

        return newNode;
    },

    /**
     * Underlying implementation of <code>setErrorMessage()
     * </code>.  This is separated out because sometimes the
     * element we wish to set the error message on is not
     * "this" element.
     *
     * @param ele   the element to set the error message on
     *
     * @param msg   the message
     *
     */
    _setErrorMessageImpl : function(ele, msg)
    {
        this._errorMessage = msg;

        if (ele)
        {
            // a null message sometimes displays as
            // a literal 'null'... so we will massage
            // the msg
            if (typeof msg != "string")
            {
                msg = "";
            }

            // alter the css class of the element, based on
            // whether or not we have a message
            if (msg == "")
            {
                Dowry.css.removeClassName(ele, this.CSS_INVALID);
            }
            else
            {
                Dowry.css.addClassName(ele, this.CSS_INVALID);
            }

            // display the message as a title (tooltip)
            ele.title = msg;
        }
    },

    /**
     * Base handling of setting the raw value from the
     * specified element.  We'll put this in its own
     * method, so we can reuse it elsewhere.
     *
     * @param ele   the element to get value from
     *
     * @param val   the element's value (the inner content)
     *
     */
    _setValueImpl : function(ele, val)
    {
        if (ele)
        {
            // treat null as empty string
            if (val == null)
            {
                val = "";
            }

            // handle html encoding unless the
            // we already have an xml node
            if (!val.nodeType)
            {
                val = document.createTextNode(val);

                // cleanup any existing child text nodes
                var childEles = ele.childNodes;
                if (childEles != null &&
                    childEles.length != null)
                {
                    var i = 0;
                    while (i < childEles.length)
                    {
                        var childEle = childEles[i];
                        if (childEle != null &&
                            childEle.nodeType == 3)
                        {
                            ele.removeChild(childEle);
                        }
                        else
                        {
                            i++;
                        }
                    }
                }
            }

            // if we are passed in an xml document,
            // then pull the root element so we can
            // import a specific element
            else if (val.nodeType == 9)
            {
                val = val.documentElement;
                val = this._importNode(ele.ownerDocument, val, true);
            }

            // any other xml node, we just need to
            // import it
            else
            {
                val = this._importNode(ele.ownerDocument, val, true);
            }

            ele.appendChild(val);
        }
    }
});